mapreader 1.2.0__tar.gz → 1.3.0__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.2.0 → mapreader-1.3.0}/PKG-INFO +1 -1
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/__init__.py +10 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/_version.py +3 -3
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/load/images.py +1 -1
- mapreader-1.3.0/mapreader/spot_text/deepsolo_runner.py +643 -0
- mapreader-1.3.0/mapreader/spot_text/dptext_detr_runner.py +434 -0
- mapreader-1.3.0/mapreader/utils/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/PKG-INFO +1 -1
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/SOURCES.txt +3 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/LICENSE +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/README.md +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/annotate/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/annotate/annotator.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/annotate/utils.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/classify/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/classify/classifier.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/classify/custom_models.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/classify/datasets.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/classify/load_annotations.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/data_structures.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/downloader.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/downloader_utils.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/sheet_downloader.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/tile_loading.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/download/tile_merging.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/load/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/load/geo_utils.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/load/loader.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/process/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/process/post_process.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/process/process.py +0 -0
- {mapreader-1.2.0/mapreader/utils → mapreader-1.3.0/mapreader/spot_text}/__init__.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/utils/compute_and_save_stats.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader/utils/slice_parallel.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/dependency_links.txt +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/entry_points.txt +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/not-zip-safe +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/requires.txt +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/mapreader.egg-info/top_level.txt +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/setup.cfg +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/setup.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/tests/test_annotator.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/tests/test_geo_pipeline.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/tests/test_import.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/tests/test_post_processing.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/tests/test_sheet_downloader.py +0 -0
- {mapreader-1.2.0 → mapreader-1.3.0}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapreader
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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
|
|
@@ -12,6 +12,16 @@ from mapreader.classify.datasets import PatchContextDataset
|
|
|
12
12
|
from mapreader.classify.classifier import ClassifierContainer
|
|
13
13
|
from mapreader.classify import custom_models
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
from mapreader.spot_text.deepsolo_runner import DeepSoloRunner
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from mapreader.spot_text.dptext_detr_runner import DPTextDETRRunner
|
|
22
|
+
except ImportError:
|
|
23
|
+
pass
|
|
24
|
+
|
|
15
25
|
from mapreader.process import process
|
|
16
26
|
|
|
17
27
|
from mapreader.annotate.annotator import Annotator
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-04-
|
|
11
|
+
"date": "2024-04-29T15:52:44+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.
|
|
14
|
+
"full-revisionid": "f256ae9b73ae9dd14d8bd72b61a86088f8f1905a",
|
|
15
|
+
"version": "1.3.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import pickle
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import adet
|
|
9
|
+
except ImportError:
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import geopandas as geopd
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from adet.config import get_cfg
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from detectron2.engine import DefaultPredictor
|
|
21
|
+
except ImportError:
|
|
22
|
+
raise ImportError("Please install Detectron2")
|
|
23
|
+
|
|
24
|
+
import matplotlib.patches as patches
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
from PIL import Image
|
|
27
|
+
from shapely import LineString, Polygon
|
|
28
|
+
|
|
29
|
+
# first assert we are using the deep solo version of adet
|
|
30
|
+
if adet.__version__ != "0.2.0-deepsolo":
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DeepSoloRunner:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
patch_df: pd.DataFrame = None,
|
|
40
|
+
parent_df: pd.DataFrame = None,
|
|
41
|
+
cfg_file: str
|
|
42
|
+
| pathlib.Path = "./DeepSolo/configs/R_50/IC15/finetune_150k_tt_mlt_13_15_textocr.yaml",
|
|
43
|
+
weights_file: str
|
|
44
|
+
| pathlib.Path = "./ic15_res50_finetune_synth-tt-mlt-13-15-textocr.pth",
|
|
45
|
+
device: str = "cpu",
|
|
46
|
+
) -> None:
|
|
47
|
+
# setup the dataframes
|
|
48
|
+
self.patch_df = patch_df
|
|
49
|
+
self.parent_df = parent_df
|
|
50
|
+
|
|
51
|
+
# set up predictions as dictionaries
|
|
52
|
+
self.patch_predictions = {}
|
|
53
|
+
self.parent_predictions = {}
|
|
54
|
+
self.geo_predictions = {}
|
|
55
|
+
|
|
56
|
+
# setup the config
|
|
57
|
+
cfg = get_cfg() # get a fresh new config
|
|
58
|
+
cfg.merge_from_file(cfg_file)
|
|
59
|
+
cfg.MODEL.WEIGHTS = weights_file
|
|
60
|
+
cfg.MODEL.DEVICE = device
|
|
61
|
+
|
|
62
|
+
self.voc_size = cfg.MODEL.TRANSFORMER.VOC_SIZE
|
|
63
|
+
self.use_customer_dictionary = cfg.MODEL.TRANSFORMER.CUSTOM_DICT
|
|
64
|
+
if self.voc_size == 96:
|
|
65
|
+
self.CTLABELS = [
|
|
66
|
+
" ",
|
|
67
|
+
"!",
|
|
68
|
+
'"',
|
|
69
|
+
"#",
|
|
70
|
+
"$",
|
|
71
|
+
"%",
|
|
72
|
+
"&",
|
|
73
|
+
"'",
|
|
74
|
+
"(",
|
|
75
|
+
")",
|
|
76
|
+
"*",
|
|
77
|
+
"+",
|
|
78
|
+
",",
|
|
79
|
+
"-",
|
|
80
|
+
".",
|
|
81
|
+
"/",
|
|
82
|
+
"0",
|
|
83
|
+
"1",
|
|
84
|
+
"2",
|
|
85
|
+
"3",
|
|
86
|
+
"4",
|
|
87
|
+
"5",
|
|
88
|
+
"6",
|
|
89
|
+
"7",
|
|
90
|
+
"8",
|
|
91
|
+
"9",
|
|
92
|
+
":",
|
|
93
|
+
";",
|
|
94
|
+
"<",
|
|
95
|
+
"=",
|
|
96
|
+
">",
|
|
97
|
+
"?",
|
|
98
|
+
"@",
|
|
99
|
+
"A",
|
|
100
|
+
"B",
|
|
101
|
+
"C",
|
|
102
|
+
"D",
|
|
103
|
+
"E",
|
|
104
|
+
"F",
|
|
105
|
+
"G",
|
|
106
|
+
"H",
|
|
107
|
+
"I",
|
|
108
|
+
"J",
|
|
109
|
+
"K",
|
|
110
|
+
"L",
|
|
111
|
+
"M",
|
|
112
|
+
"N",
|
|
113
|
+
"O",
|
|
114
|
+
"P",
|
|
115
|
+
"Q",
|
|
116
|
+
"R",
|
|
117
|
+
"S",
|
|
118
|
+
"T",
|
|
119
|
+
"U",
|
|
120
|
+
"V",
|
|
121
|
+
"W",
|
|
122
|
+
"X",
|
|
123
|
+
"Y",
|
|
124
|
+
"Z",
|
|
125
|
+
"[",
|
|
126
|
+
"\\",
|
|
127
|
+
"]",
|
|
128
|
+
"^",
|
|
129
|
+
"_",
|
|
130
|
+
"`",
|
|
131
|
+
"a",
|
|
132
|
+
"b",
|
|
133
|
+
"c",
|
|
134
|
+
"d",
|
|
135
|
+
"e",
|
|
136
|
+
"f",
|
|
137
|
+
"g",
|
|
138
|
+
"h",
|
|
139
|
+
"i",
|
|
140
|
+
"j",
|
|
141
|
+
"k",
|
|
142
|
+
"l",
|
|
143
|
+
"m",
|
|
144
|
+
"n",
|
|
145
|
+
"o",
|
|
146
|
+
"p",
|
|
147
|
+
"q",
|
|
148
|
+
"r",
|
|
149
|
+
"s",
|
|
150
|
+
"t",
|
|
151
|
+
"u",
|
|
152
|
+
"v",
|
|
153
|
+
"w",
|
|
154
|
+
"x",
|
|
155
|
+
"y",
|
|
156
|
+
"z",
|
|
157
|
+
"{",
|
|
158
|
+
"|",
|
|
159
|
+
"}",
|
|
160
|
+
"~",
|
|
161
|
+
]
|
|
162
|
+
elif self.voc_size == 37:
|
|
163
|
+
self.CTLABELS = [
|
|
164
|
+
"a",
|
|
165
|
+
"b",
|
|
166
|
+
"c",
|
|
167
|
+
"d",
|
|
168
|
+
"e",
|
|
169
|
+
"f",
|
|
170
|
+
"g",
|
|
171
|
+
"h",
|
|
172
|
+
"i",
|
|
173
|
+
"j",
|
|
174
|
+
"k",
|
|
175
|
+
"l",
|
|
176
|
+
"m",
|
|
177
|
+
"n",
|
|
178
|
+
"o",
|
|
179
|
+
"p",
|
|
180
|
+
"q",
|
|
181
|
+
"r",
|
|
182
|
+
"s",
|
|
183
|
+
"t",
|
|
184
|
+
"u",
|
|
185
|
+
"v",
|
|
186
|
+
"w",
|
|
187
|
+
"x",
|
|
188
|
+
"y",
|
|
189
|
+
"z",
|
|
190
|
+
"0",
|
|
191
|
+
"1",
|
|
192
|
+
"2",
|
|
193
|
+
"3",
|
|
194
|
+
"4",
|
|
195
|
+
"5",
|
|
196
|
+
"6",
|
|
197
|
+
"7",
|
|
198
|
+
"8",
|
|
199
|
+
"9",
|
|
200
|
+
]
|
|
201
|
+
else:
|
|
202
|
+
with open(self.use_customer_dictionary, "rb") as fp:
|
|
203
|
+
self.CTLABELS = pickle.load(fp)
|
|
204
|
+
# voc_size includes the unknown class, which is not in self.CTABLES
|
|
205
|
+
assert int(self.voc_size - 1) == len(
|
|
206
|
+
self.CTLABELS
|
|
207
|
+
), f"voc_size is not matched dictionary size, got {int(self.voc_size - 1)} and {len(self.CTLABELS)}."
|
|
208
|
+
|
|
209
|
+
# setup the predictor
|
|
210
|
+
self.predictor = DefaultPredictor(cfg)
|
|
211
|
+
|
|
212
|
+
def run_all(
|
|
213
|
+
self,
|
|
214
|
+
patch_df: pd.DataFrame = None,
|
|
215
|
+
return_dataframe: bool = False,
|
|
216
|
+
) -> dict | pd.DataFrame:
|
|
217
|
+
"""Run the model on all images in the patch dataframe.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
patch_df : pd.DataFrame, optional
|
|
222
|
+
Dataframe containing patch information, by default None.
|
|
223
|
+
return_dataframe : bool, optional
|
|
224
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
dict or pd.DataFrame
|
|
229
|
+
A dictionary of predictions for each patch image or a DataFrame if `as_dataframe` is True.
|
|
230
|
+
"""
|
|
231
|
+
if patch_df is None:
|
|
232
|
+
if self.patch_df is not None:
|
|
233
|
+
patch_df = self.patch_df
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError("[ERROR] Please provide a `patch_df`")
|
|
236
|
+
img_paths = patch_df["image_path"].to_list()
|
|
237
|
+
|
|
238
|
+
patch_predictions = self.run_on_images(
|
|
239
|
+
img_paths, return_dataframe=return_dataframe
|
|
240
|
+
)
|
|
241
|
+
return patch_predictions
|
|
242
|
+
|
|
243
|
+
def run_on_images(
|
|
244
|
+
self,
|
|
245
|
+
img_paths: str | pathlib.Path | list,
|
|
246
|
+
return_dataframe: bool = False,
|
|
247
|
+
) -> dict | pd.DataFrame:
|
|
248
|
+
"""Run the model on a list of images.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
img_paths : str, pathlib.Path or list
|
|
253
|
+
A list of image paths to run the model on.
|
|
254
|
+
return_dataframe : bool, optional
|
|
255
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
dict or pd.DataFrame
|
|
260
|
+
A dictionary of predictions for each image or a DataFrame if `as_dataframe` is True.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
if isinstance(img_paths, (str, pathlib.Path)):
|
|
264
|
+
img_paths = [img_paths]
|
|
265
|
+
|
|
266
|
+
for img_path in img_paths:
|
|
267
|
+
_ = self.run_on_image(img_path, return_outputs=False)
|
|
268
|
+
|
|
269
|
+
if return_dataframe:
|
|
270
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
271
|
+
return self.patch_predictions
|
|
272
|
+
|
|
273
|
+
def run_on_image(
|
|
274
|
+
self,
|
|
275
|
+
img_path: str | pathlib.Path,
|
|
276
|
+
return_outputs=False,
|
|
277
|
+
return_dataframe: bool = False,
|
|
278
|
+
) -> dict | pd.DataFrame:
|
|
279
|
+
"""Run the model on a single image.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
img_path : str or pathlib.Path
|
|
284
|
+
The path to the image to run the model on.
|
|
285
|
+
return_outputs : bool, optional
|
|
286
|
+
Whether to return the outputs direct from the model, by default False
|
|
287
|
+
return_dataframe : bool, optional
|
|
288
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
dict or pd.DataFrame
|
|
293
|
+
The predictions for the image or the outputs from the model if `return_outputs` is True.
|
|
294
|
+
"""
|
|
295
|
+
# load image
|
|
296
|
+
img = Image.open(img_path).convert("RGB")
|
|
297
|
+
img_array = np.array(img)
|
|
298
|
+
|
|
299
|
+
# run inference
|
|
300
|
+
outputs = self.predictor(img_array)
|
|
301
|
+
outputs["image_id"] = os.path.basename(img_path)
|
|
302
|
+
outputs["img_path"] = img_path
|
|
303
|
+
|
|
304
|
+
if return_outputs:
|
|
305
|
+
return outputs
|
|
306
|
+
|
|
307
|
+
self.get_patch_predictions(outputs)
|
|
308
|
+
|
|
309
|
+
if return_dataframe:
|
|
310
|
+
return self._dict_to_dataframe(self.patch_predictions, patch=True)
|
|
311
|
+
return self.patch_predictions
|
|
312
|
+
|
|
313
|
+
def get_patch_predictions(
|
|
314
|
+
self,
|
|
315
|
+
outputs: dict,
|
|
316
|
+
return_dataframe: bool = False,
|
|
317
|
+
) -> dict | pd.DataFrame:
|
|
318
|
+
"""Post process the model outputs to get patch predictions.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
outputs : dict
|
|
323
|
+
The outputs from the model.
|
|
324
|
+
return_dataframe : bool, optional
|
|
325
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
dict or pd.DataFrame
|
|
330
|
+
A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
|
|
331
|
+
"""
|
|
332
|
+
# key for predictions
|
|
333
|
+
image_id = outputs["image_id"]
|
|
334
|
+
self.patch_predictions[image_id] = []
|
|
335
|
+
|
|
336
|
+
# get instances
|
|
337
|
+
instances = outputs["instances"].to("cpu")
|
|
338
|
+
ctrl_pnts = instances.ctrl_points.numpy()
|
|
339
|
+
scores = instances.scores.tolist()
|
|
340
|
+
recs = instances.recs
|
|
341
|
+
bd_pts = np.asarray(instances.bd)
|
|
342
|
+
|
|
343
|
+
self._post_process(image_id, ctrl_pnts, scores, recs, bd_pts)
|
|
344
|
+
|
|
345
|
+
if return_dataframe:
|
|
346
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
347
|
+
return self.patch_predictions
|
|
348
|
+
|
|
349
|
+
def _process_ctrl_pnt(self, pnt):
|
|
350
|
+
points = pnt.reshape(-1, 2)
|
|
351
|
+
return points
|
|
352
|
+
|
|
353
|
+
def _ctc_decode_recognition(self, rec):
|
|
354
|
+
last_char = "###"
|
|
355
|
+
s = ""
|
|
356
|
+
for c in rec:
|
|
357
|
+
c = int(c)
|
|
358
|
+
if c < self.voc_size - 1:
|
|
359
|
+
if last_char != c:
|
|
360
|
+
if self.voc_size == 37 or self.voc_size == 96:
|
|
361
|
+
s += self.CTLABELS[c]
|
|
362
|
+
last_char = c
|
|
363
|
+
else:
|
|
364
|
+
s += str(chr(self.CTLABELS[c]))
|
|
365
|
+
last_char = c
|
|
366
|
+
else:
|
|
367
|
+
last_char = "###"
|
|
368
|
+
return s
|
|
369
|
+
|
|
370
|
+
def _post_process(self, image_id, ctrl_pnts, scores, recs, bd_pnts, alpha=0.4):
|
|
371
|
+
for ctrl_pnt, score, rec, bd in zip(ctrl_pnts, scores, recs, bd_pnts):
|
|
372
|
+
# draw polygons
|
|
373
|
+
if bd is not None:
|
|
374
|
+
bd = np.hsplit(bd, 2)
|
|
375
|
+
bd = np.vstack([bd[0], bd[1][::-1]])
|
|
376
|
+
polygon = Polygon(bd)
|
|
377
|
+
|
|
378
|
+
# draw center lines
|
|
379
|
+
line = self._process_ctrl_pnt(ctrl_pnt)
|
|
380
|
+
line = LineString(line)
|
|
381
|
+
|
|
382
|
+
# draw text
|
|
383
|
+
text = self._ctc_decode_recognition(rec)
|
|
384
|
+
if self.voc_size == 37:
|
|
385
|
+
text = text.upper()
|
|
386
|
+
# text = "{:.2f}: {}".format(score, text)
|
|
387
|
+
text = f"{text}"
|
|
388
|
+
score = f"{score:.2f}"
|
|
389
|
+
|
|
390
|
+
self.patch_predictions[image_id].append([polygon, text, score])
|
|
391
|
+
|
|
392
|
+
def convert_to_parent_pixel_bounds(
|
|
393
|
+
self,
|
|
394
|
+
patch_df: pd.DataFrame = None,
|
|
395
|
+
return_dataframe: bool = False,
|
|
396
|
+
) -> dict | pd.DataFrame:
|
|
397
|
+
"""Convert the patch predictions to parent predictions by converting pixel bounds.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
patch_df : pd.DataFrame, optional
|
|
402
|
+
Dataframe containing patch information, by default None
|
|
403
|
+
return_dataframe : bool, optional
|
|
404
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
405
|
+
|
|
406
|
+
Returns
|
|
407
|
+
-------
|
|
408
|
+
dict or pd.DataFrame
|
|
409
|
+
A dictionary of predictions for each parent image or a DataFrame if `as_dataframe` is True.
|
|
410
|
+
|
|
411
|
+
Raises
|
|
412
|
+
------
|
|
413
|
+
ValueError
|
|
414
|
+
If `patch_df` is not available.
|
|
415
|
+
"""
|
|
416
|
+
if patch_df is None:
|
|
417
|
+
if self.patch_df is not None:
|
|
418
|
+
patch_df = self.patch_df
|
|
419
|
+
else:
|
|
420
|
+
raise ValueError("[ERROR] Please provide a `patch_df`")
|
|
421
|
+
|
|
422
|
+
for image_id, prediction in self.patch_predictions.items():
|
|
423
|
+
parent_id = patch_df.loc[image_id, "parent_id"]
|
|
424
|
+
if parent_id not in self.parent_predictions.keys():
|
|
425
|
+
self.parent_predictions[parent_id] = []
|
|
426
|
+
|
|
427
|
+
for instance in prediction:
|
|
428
|
+
polygon = instance[0]
|
|
429
|
+
|
|
430
|
+
xx, yy = (np.array(i) for i in polygon.exterior.xy)
|
|
431
|
+
xx = xx + patch_df.loc[image_id, "pixel_bounds"][0] # add min_x
|
|
432
|
+
yy = yy + patch_df.loc[image_id, "pixel_bounds"][1] # add min_y
|
|
433
|
+
|
|
434
|
+
parent_polygon = Polygon(zip(xx, yy))
|
|
435
|
+
self.parent_predictions[parent_id].append(
|
|
436
|
+
[parent_polygon, instance[1], instance[2]]
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if return_dataframe:
|
|
440
|
+
return self._dict_to_dataframe(self.parent_predictions, geo=False)
|
|
441
|
+
return self.parent_predictions
|
|
442
|
+
|
|
443
|
+
def convert_to_coords(
|
|
444
|
+
self,
|
|
445
|
+
parent_df: pd.DataFrame = None,
|
|
446
|
+
return_dataframe: bool = False,
|
|
447
|
+
) -> dict | pd.DataFrame:
|
|
448
|
+
"""Convert the parent predictions to georeferenced predictions by converting pixel bounds to coordinates.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
parent_df : pd.DataFrame, optional
|
|
453
|
+
Dataframe containing parent image information, by default None
|
|
454
|
+
return_dataframe : bool, optional
|
|
455
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
dict or pd.DataFrame
|
|
460
|
+
A dictionary of predictions for each parent image or a DataFrame if `as_dataframe` is True.
|
|
461
|
+
|
|
462
|
+
Raises
|
|
463
|
+
------
|
|
464
|
+
ValueError
|
|
465
|
+
If `parent_df` is not available.
|
|
466
|
+
"""
|
|
467
|
+
if parent_df is None:
|
|
468
|
+
if self.parent_df is not None:
|
|
469
|
+
parent_df = self.parent_df
|
|
470
|
+
else:
|
|
471
|
+
raise ValueError("[ERROR] Please provide a `parent_df`")
|
|
472
|
+
|
|
473
|
+
if self.parent_predictions == {}:
|
|
474
|
+
print("[INFO] Converting patch pixel bounds to parent pixel bounds.")
|
|
475
|
+
_ = self.convert_to_parent_pixel_bounds()
|
|
476
|
+
|
|
477
|
+
for parent_id, prediction in self.parent_predictions.items():
|
|
478
|
+
if parent_id not in self.geo_predictions.keys():
|
|
479
|
+
self.geo_predictions[parent_id] = []
|
|
480
|
+
|
|
481
|
+
for instance in prediction:
|
|
482
|
+
polygon = instance[0]
|
|
483
|
+
|
|
484
|
+
xx, yy = (np.array(i) for i in polygon.exterior.xy)
|
|
485
|
+
xx = (
|
|
486
|
+
xx * parent_df.loc[parent_id, "dlon"]
|
|
487
|
+
+ parent_df.loc[parent_id, "coordinates"][0]
|
|
488
|
+
)
|
|
489
|
+
yy = (
|
|
490
|
+
parent_df.loc[parent_id, "coordinates"][3]
|
|
491
|
+
- yy * parent_df.loc[parent_id, "dlat"]
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
crs = parent_df.loc[parent_id, "crs"]
|
|
495
|
+
|
|
496
|
+
parent_polygon_geo = Polygon(zip(xx, yy))
|
|
497
|
+
self.geo_predictions[parent_id].append(
|
|
498
|
+
[parent_polygon_geo, crs, instance[1], instance[2]]
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if return_dataframe:
|
|
502
|
+
return self._dict_to_dataframe(self.geo_predictions, geo=True)
|
|
503
|
+
return self.geo_predictions
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _dict_to_dataframe(
|
|
507
|
+
preds: dict,
|
|
508
|
+
geo: bool = False,
|
|
509
|
+
) -> pd.DataFrame:
|
|
510
|
+
"""Convert the predictions dictionary to a pandas DataFrame.
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
preds : dict
|
|
515
|
+
A dictionary of predictions.
|
|
516
|
+
geo : bool, optional
|
|
517
|
+
Whether the dictionary is georeferenced coords (or pixel bounds), by default True
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
pd.DataFrame
|
|
522
|
+
A pandas DataFrame containing the predictions.
|
|
523
|
+
"""
|
|
524
|
+
if geo:
|
|
525
|
+
columns = ["polygon", "crs", "text", "score"]
|
|
526
|
+
else:
|
|
527
|
+
columns = ["polygon", "text", "score"]
|
|
528
|
+
|
|
529
|
+
preds_df = pd.concat(
|
|
530
|
+
pd.DataFrame(
|
|
531
|
+
preds[k],
|
|
532
|
+
index=np.full(len(preds[k]), k),
|
|
533
|
+
columns=columns,
|
|
534
|
+
)
|
|
535
|
+
for k in preds.keys()
|
|
536
|
+
)
|
|
537
|
+
preds_df.index.name = "image_id"
|
|
538
|
+
preds_df.reset_index(inplace=True) # reset index to get image_id as a column
|
|
539
|
+
return preds_df
|
|
540
|
+
|
|
541
|
+
def save_to_geojson(
|
|
542
|
+
self,
|
|
543
|
+
save_path: str | pathlib.Path = "./deepsolo_text_outputs.geojson",
|
|
544
|
+
) -> None:
|
|
545
|
+
"""Save the georeferenced predictions to a GeoJSON file.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
save_path : str | pathlib.Path, optional
|
|
550
|
+
Path to save the GeoJSON file, by default "./deepsolo_text_outputs.geojson"
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
geo_df = self._dict_to_dataframe(self.geo_predictions, geo=True)
|
|
554
|
+
|
|
555
|
+
# get the crs (should be the same for all polygons)
|
|
556
|
+
assert geo_df["crs"].nunique() == 1
|
|
557
|
+
crs = geo_df["crs"].unique()[0]
|
|
558
|
+
|
|
559
|
+
geo_df = geopd.GeoDataFrame(geo_df, geometry="polygon", crs=crs)
|
|
560
|
+
geo_df.to_file(save_path, driver="GeoJSON")
|
|
561
|
+
|
|
562
|
+
def show(
|
|
563
|
+
self,
|
|
564
|
+
image_id: str,
|
|
565
|
+
figsize: tuple | None = (10, 10),
|
|
566
|
+
border_color: str | None = "r",
|
|
567
|
+
text_color: str | None = "b",
|
|
568
|
+
image_width_resolution: int | None = None,
|
|
569
|
+
return_fig: bool = False,
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Show the predictions on an image.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
image_id : str
|
|
576
|
+
The image ID to show the predictions on.
|
|
577
|
+
figsize : tuple | None, optional
|
|
578
|
+
The size of the figure, by default (10, 10)
|
|
579
|
+
border_color : str | None, optional
|
|
580
|
+
The color of the border of the polygons, by default "r"
|
|
581
|
+
text_color : str | None, optional
|
|
582
|
+
The color of the text, by default "b"
|
|
583
|
+
image_width_resolution : int | None, optional
|
|
584
|
+
The maximum resolution of the image width, by default None
|
|
585
|
+
return_fig : bool, optional
|
|
586
|
+
Whether to return the figure, by default False
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
fig
|
|
591
|
+
The matplotlib figure if `return_fig` is True.
|
|
592
|
+
|
|
593
|
+
Raises
|
|
594
|
+
------
|
|
595
|
+
ValueError
|
|
596
|
+
If the image ID is not found in the patch or parent predictions.
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
if image_id in self.patch_predictions.keys():
|
|
600
|
+
preds = self.patch_predictions
|
|
601
|
+
image_path = self.patch_df.loc[image_id, "image_path"]
|
|
602
|
+
|
|
603
|
+
elif image_id in self.parent_predictions.keys():
|
|
604
|
+
preds = self.parent_predictions
|
|
605
|
+
image_path = self.parent_df.loc[image_id, "image_path"]
|
|
606
|
+
|
|
607
|
+
else:
|
|
608
|
+
raise ValueError(
|
|
609
|
+
f"[ERROR] {image_id} not found in patch or parent predictions."
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
img = Image.open(image_path)
|
|
613
|
+
|
|
614
|
+
# if image_width_resolution is specified, resize the image
|
|
615
|
+
if image_width_resolution:
|
|
616
|
+
new_width = int(image_width_resolution)
|
|
617
|
+
rescale_factor = new_width / img.width
|
|
618
|
+
new_height = int(img.height * rescale_factor)
|
|
619
|
+
img = img.resize((new_width, new_height), Image.LANCZOS)
|
|
620
|
+
|
|
621
|
+
fig = plt.figure(figsize=figsize)
|
|
622
|
+
ax = plt.gca()
|
|
623
|
+
|
|
624
|
+
# check if grayscale
|
|
625
|
+
if len(img.getbands()) == 1:
|
|
626
|
+
ax.imshow(img, cmap="gray", vmin=0, vmax=255, zorder=1)
|
|
627
|
+
else:
|
|
628
|
+
ax.imshow(img, zorder=1)
|
|
629
|
+
|
|
630
|
+
for instance in preds[image_id]:
|
|
631
|
+
polygon = np.array(instance[0].exterior.coords.xy)
|
|
632
|
+
center = instance[0].centroid.coords.xy
|
|
633
|
+
patch = patches.Polygon(polygon.T, edgecolor=border_color, facecolor="none")
|
|
634
|
+
ax.add_patch(patch)
|
|
635
|
+
ax.text(
|
|
636
|
+
center[0][0], center[1][0], instance[1], fontsize=8, color=text_color
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
plt.axis("off")
|
|
640
|
+
plt.title(image_id)
|
|
641
|
+
|
|
642
|
+
if return_fig:
|
|
643
|
+
return fig
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import adet
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise ImportError(
|
|
10
|
+
"Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
import geopandas as geopd
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
from adet.config import get_cfg
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from detectron2.engine import DefaultPredictor
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError("Please install Detectron2")
|
|
22
|
+
|
|
23
|
+
import matplotlib.patches as patches
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from PIL import Image
|
|
26
|
+
from shapely import Polygon
|
|
27
|
+
|
|
28
|
+
# first assert we are using the dptext detr version of adet
|
|
29
|
+
if adet.__version__ != "0.2.0-dptext-detr":
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DPTextDETRRunner:
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
patch_df: pd.DataFrame = None,
|
|
39
|
+
parent_df: pd.DataFrame = None,
|
|
40
|
+
cfg_file: str
|
|
41
|
+
| pathlib.Path = "./DPText-DETR/configs/DPText_DETR/ArT/R_50_poly.yaml",
|
|
42
|
+
weights_file: str | pathlib.Path = "./art_final.pth",
|
|
43
|
+
device: str = "cpu",
|
|
44
|
+
) -> None:
|
|
45
|
+
# first assert we are using the deep solo version of adet
|
|
46
|
+
assert adet.__version__ == "0.2.0-dptext-detr"
|
|
47
|
+
|
|
48
|
+
# setup the dataframes
|
|
49
|
+
self.patch_df = patch_df
|
|
50
|
+
self.parent_df = parent_df
|
|
51
|
+
|
|
52
|
+
# set up predictions as dictionaries
|
|
53
|
+
self.patch_predictions = {}
|
|
54
|
+
self.parent_predictions = {}
|
|
55
|
+
self.geo_predictions = {}
|
|
56
|
+
|
|
57
|
+
# setup the config
|
|
58
|
+
cfg = get_cfg() # get a fresh new config
|
|
59
|
+
cfg.merge_from_file(cfg_file)
|
|
60
|
+
cfg.MODEL.WEIGHTS = weights_file
|
|
61
|
+
cfg.MODEL.DEVICE = device
|
|
62
|
+
|
|
63
|
+
# setup the predictor
|
|
64
|
+
self.predictor = DefaultPredictor(cfg)
|
|
65
|
+
|
|
66
|
+
def run_all(
|
|
67
|
+
self,
|
|
68
|
+
patch_df: pd.DataFrame = None,
|
|
69
|
+
return_dataframe: bool = False,
|
|
70
|
+
) -> dict | pd.DataFrame:
|
|
71
|
+
"""Run the model on all images in the patch dataframe.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
patch_df : pd.DataFrame, optional
|
|
76
|
+
Dataframe containing patch information, by default None.
|
|
77
|
+
return_dataframe : bool, optional
|
|
78
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
dict or pd.DataFrame
|
|
83
|
+
A dictionary of predictions for each patch image or a DataFrame if `as_dataframe` is True.
|
|
84
|
+
"""
|
|
85
|
+
if patch_df is None:
|
|
86
|
+
if self.patch_df is not None:
|
|
87
|
+
patch_df = self.patch_df
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError("[ERROR] Please provide a `patch_df`")
|
|
90
|
+
img_paths = patch_df["image_path"].to_list()
|
|
91
|
+
|
|
92
|
+
patch_predictions = self.run_on_images(
|
|
93
|
+
img_paths, return_dataframe=return_dataframe
|
|
94
|
+
)
|
|
95
|
+
return patch_predictions
|
|
96
|
+
|
|
97
|
+
def run_on_images(
|
|
98
|
+
self,
|
|
99
|
+
img_paths: str | pathlib.Path | list,
|
|
100
|
+
return_dataframe: bool = False,
|
|
101
|
+
) -> dict | pd.DataFrame:
|
|
102
|
+
"""Run the model on a list of images.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
img_paths : str, pathlib.Path or list
|
|
107
|
+
A list of image paths to run the model on.
|
|
108
|
+
return_dataframe : bool, optional
|
|
109
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
dict or pd.DataFrame
|
|
114
|
+
A dictionary of predictions for each image or a DataFrame if `as_dataframe` is True.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
if isinstance(img_paths, (str, pathlib.Path)):
|
|
118
|
+
img_paths = [img_paths]
|
|
119
|
+
|
|
120
|
+
for img_path in img_paths:
|
|
121
|
+
_ = self.run_on_image(img_path, return_outputs=False)
|
|
122
|
+
|
|
123
|
+
if return_dataframe:
|
|
124
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
125
|
+
return self.patch_predictions
|
|
126
|
+
|
|
127
|
+
def run_on_image(
|
|
128
|
+
self,
|
|
129
|
+
img_path: str | pathlib.Path,
|
|
130
|
+
return_outputs=False,
|
|
131
|
+
return_dataframe: bool = False,
|
|
132
|
+
) -> dict | pd.DataFrame:
|
|
133
|
+
"""Run the model on a single image.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
img_path : str or pathlib.Path
|
|
138
|
+
The path to the image to run the model on.
|
|
139
|
+
return_outputs : bool, optional
|
|
140
|
+
Whether to return the outputs direct from the model, by default False
|
|
141
|
+
return_dataframe : bool, optional
|
|
142
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
dict or pd.DataFrame
|
|
147
|
+
The predictions for the image or the outputs from the model if `return_outputs` is True.
|
|
148
|
+
"""
|
|
149
|
+
# load image
|
|
150
|
+
img = Image.open(img_path).convert("RGB")
|
|
151
|
+
img_array = np.array(img)
|
|
152
|
+
|
|
153
|
+
# run inference
|
|
154
|
+
outputs = self.predictor(img_array)
|
|
155
|
+
outputs["image_id"] = os.path.basename(img_path)
|
|
156
|
+
outputs["img_path"] = img_path
|
|
157
|
+
|
|
158
|
+
if return_outputs:
|
|
159
|
+
return outputs
|
|
160
|
+
|
|
161
|
+
self.get_patch_predictions(outputs)
|
|
162
|
+
|
|
163
|
+
if return_dataframe:
|
|
164
|
+
return self._dict_to_dataframe(self.patch_predictions, patch=True)
|
|
165
|
+
return self.patch_predictions
|
|
166
|
+
|
|
167
|
+
def get_patch_predictions(
|
|
168
|
+
self,
|
|
169
|
+
outputs: dict,
|
|
170
|
+
return_dataframe: bool = False,
|
|
171
|
+
) -> dict | pd.DataFrame:
|
|
172
|
+
"""Post process the model outputs to get patch predictions.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
outputs : dict
|
|
177
|
+
The outputs from the model.
|
|
178
|
+
return_dataframe : bool, optional
|
|
179
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
dict or pd.DataFrame
|
|
184
|
+
A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
|
|
185
|
+
"""
|
|
186
|
+
# key for predictions
|
|
187
|
+
image_id = outputs["image_id"]
|
|
188
|
+
self.patch_predictions[image_id] = []
|
|
189
|
+
|
|
190
|
+
# get instances
|
|
191
|
+
instances = outputs["instances"].to("cpu")
|
|
192
|
+
scores = instances.scores.tolist()
|
|
193
|
+
pred_classes = instances.pred_classes.tolist()
|
|
194
|
+
bd_pts = np.asarray(instances.polygons)
|
|
195
|
+
|
|
196
|
+
self._post_process(image_id, scores, pred_classes, bd_pts)
|
|
197
|
+
|
|
198
|
+
if return_dataframe:
|
|
199
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
200
|
+
return self.patch_predictions
|
|
201
|
+
|
|
202
|
+
def _post_process(self, image_id, scores, pred_classes, bd_pnts):
|
|
203
|
+
for score, _pred_class, bd in zip(scores, pred_classes, bd_pnts):
|
|
204
|
+
# draw polygons
|
|
205
|
+
if bd is not None:
|
|
206
|
+
bd = bd.reshape(-1, 2)
|
|
207
|
+
polygon = Polygon(bd)
|
|
208
|
+
|
|
209
|
+
score = f"{score:.2f}"
|
|
210
|
+
|
|
211
|
+
self.patch_predictions[image_id].append([polygon, score])
|
|
212
|
+
|
|
213
|
+
def convert_to_parent_pixel_bounds(
|
|
214
|
+
self,
|
|
215
|
+
patch_df: pd.DataFrame = None,
|
|
216
|
+
return_dataframe: bool = False,
|
|
217
|
+
) -> dict | pd.DataFrame:
|
|
218
|
+
"""Convert the patch predictions to parent predictions by converting pixel bounds.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
patch_df : pd.DataFrame, optional
|
|
223
|
+
Dataframe containing patch information, by default None
|
|
224
|
+
return_dataframe : bool, optional
|
|
225
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
dict or pd.DataFrame
|
|
230
|
+
A dictionary of predictions for each parent image or a DataFrame if `as_dataframe` is True.
|
|
231
|
+
|
|
232
|
+
Raises
|
|
233
|
+
------
|
|
234
|
+
ValueError
|
|
235
|
+
If `patch_df` is not available.
|
|
236
|
+
"""
|
|
237
|
+
if patch_df is None:
|
|
238
|
+
if self.patch_df is not None:
|
|
239
|
+
patch_df = self.patch_df
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError("[ERROR] Please provide a `patch_df`")
|
|
242
|
+
|
|
243
|
+
for image_id, prediction in self.patch_predictions.items():
|
|
244
|
+
parent_id = patch_df.loc[image_id, "parent_id"]
|
|
245
|
+
if parent_id not in self.parent_predictions.keys():
|
|
246
|
+
self.parent_predictions[parent_id] = []
|
|
247
|
+
|
|
248
|
+
for instance in prediction:
|
|
249
|
+
polygon = instance[0]
|
|
250
|
+
|
|
251
|
+
xx, yy = (np.array(i) for i in polygon.exterior.xy)
|
|
252
|
+
xx = xx + patch_df.loc[image_id, "pixel_bounds"][0] # add min_x
|
|
253
|
+
yy = yy + patch_df.loc[image_id, "pixel_bounds"][1] # add min_y
|
|
254
|
+
|
|
255
|
+
parent_polygon = Polygon(zip(xx, yy))
|
|
256
|
+
self.parent_predictions[parent_id].append([parent_polygon, instance[1]])
|
|
257
|
+
|
|
258
|
+
if return_dataframe:
|
|
259
|
+
return self._dict_to_dataframe(self.parent_predictions, geo=False)
|
|
260
|
+
return self.parent_predictions
|
|
261
|
+
|
|
262
|
+
def convert_to_coords(
|
|
263
|
+
self,
|
|
264
|
+
parent_df: pd.DataFrame = None,
|
|
265
|
+
return_dataframe: bool = False,
|
|
266
|
+
) -> dict | pd.DataFrame:
|
|
267
|
+
"""Convert the parent predictions to georeferenced predictions by converting pixel bounds to coordinates.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
parent_df : pd.DataFrame, optional
|
|
272
|
+
Dataframe containing parent image information, by default None
|
|
273
|
+
return_dataframe : bool, optional
|
|
274
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
dict or pd.DataFrame
|
|
279
|
+
A dictionary of predictions for each parent image or a DataFrame if `as_dataframe` is True.
|
|
280
|
+
|
|
281
|
+
Raises
|
|
282
|
+
------
|
|
283
|
+
ValueError
|
|
284
|
+
If `parent_df` is not available.
|
|
285
|
+
"""
|
|
286
|
+
if parent_df is None:
|
|
287
|
+
if self.parent_df is not None:
|
|
288
|
+
parent_df = self.parent_df
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError("[ERROR] Please provide a `parent_df`")
|
|
291
|
+
|
|
292
|
+
if self.parent_predictions == {}:
|
|
293
|
+
print("[INFO] Converting patch pixel bounds to parent pixel bounds.")
|
|
294
|
+
_ = self.convert_to_parent_pixel_bounds()
|
|
295
|
+
|
|
296
|
+
for parent_id, prediction in self.parent_predictions.items():
|
|
297
|
+
if parent_id not in self.geo_predictions.keys():
|
|
298
|
+
self.geo_predictions[parent_id] = []
|
|
299
|
+
|
|
300
|
+
for instance in prediction:
|
|
301
|
+
polygon = instance[0]
|
|
302
|
+
|
|
303
|
+
xx, yy = (np.array(i) for i in polygon.exterior.xy)
|
|
304
|
+
xx = (
|
|
305
|
+
xx * parent_df.loc[parent_id, "dlon"]
|
|
306
|
+
+ parent_df.loc[parent_id, "coordinates"][0]
|
|
307
|
+
)
|
|
308
|
+
yy = (
|
|
309
|
+
parent_df.loc[parent_id, "coordinates"][3]
|
|
310
|
+
- yy * parent_df.loc[parent_id, "dlat"]
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
crs = parent_df.loc[parent_id, "crs"]
|
|
314
|
+
|
|
315
|
+
parent_polygon_geo = Polygon(zip(xx, yy))
|
|
316
|
+
self.geo_predictions[parent_id].append(
|
|
317
|
+
[parent_polygon_geo, crs, instance[1]]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if return_dataframe:
|
|
321
|
+
return self._dict_to_dataframe(self.geo_predictions, geo=True)
|
|
322
|
+
return self.geo_predictions
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _dict_to_dataframe(
|
|
326
|
+
preds: dict,
|
|
327
|
+
geo: bool = False,
|
|
328
|
+
) -> pd.DataFrame:
|
|
329
|
+
"""Convert the predictions dictionary to a pandas DataFrame.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
preds : dict
|
|
334
|
+
A dictionary of predictions.
|
|
335
|
+
geo : bool, optional
|
|
336
|
+
Whether the dictionary is georeferenced coords (or pixel bounds), by default True
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
pd.DataFrame
|
|
341
|
+
A pandas DataFrame containing the predictions.
|
|
342
|
+
"""
|
|
343
|
+
if geo:
|
|
344
|
+
columns = ["polygon", "crs", "score"]
|
|
345
|
+
else:
|
|
346
|
+
columns = ["polygon", "score"]
|
|
347
|
+
|
|
348
|
+
preds_df = pd.concat(
|
|
349
|
+
pd.DataFrame(
|
|
350
|
+
preds[k],
|
|
351
|
+
index=np.full(len(preds[k]), k),
|
|
352
|
+
columns=columns,
|
|
353
|
+
)
|
|
354
|
+
for k in preds.keys()
|
|
355
|
+
)
|
|
356
|
+
preds_df.index.name = "image_id"
|
|
357
|
+
preds_df.reset_index(inplace=True)
|
|
358
|
+
return preds_df
|
|
359
|
+
|
|
360
|
+
def save_to_geojson(
|
|
361
|
+
self,
|
|
362
|
+
save_path: str | pathlib.Path = "./dptext-detr_text_outputs.geojson",
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Save the georeferenced predictions to a GeoJSON file.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
save_path : str | pathlib.Path, optional
|
|
369
|
+
Path to save the GeoJSON file, by default "./deepsolo_text_outputs.geojson"
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
geo_df = self._dict_to_dataframe(self.geo_predictions, geo=True)
|
|
373
|
+
|
|
374
|
+
# get the crs (should be the same for all polygons)
|
|
375
|
+
assert geo_df["crs"].nunique() == 1
|
|
376
|
+
crs = geo_df["crs"].unique()[0]
|
|
377
|
+
|
|
378
|
+
geo_df = geopd.GeoDataFrame(geo_df, geometry="polygon", crs=crs)
|
|
379
|
+
geo_df.to_file(save_path, driver="GeoJSON")
|
|
380
|
+
|
|
381
|
+
def show(
|
|
382
|
+
self,
|
|
383
|
+
image_id: str,
|
|
384
|
+
figsize: tuple | None = (10, 10),
|
|
385
|
+
border_color: str | None = "r",
|
|
386
|
+
text_color: str | None = "b",
|
|
387
|
+
image_width_resolution: int | None = None,
|
|
388
|
+
return_fig: bool = False,
|
|
389
|
+
) -> None:
|
|
390
|
+
if image_id in self.patch_predictions.keys():
|
|
391
|
+
preds = self.patch_predictions
|
|
392
|
+
image_path = self.patch_df.loc[image_id, "image_path"]
|
|
393
|
+
|
|
394
|
+
elif image_id in self.parent_predictions.keys():
|
|
395
|
+
preds = self.parent_predictions
|
|
396
|
+
image_path = self.parent_df.loc[image_id, "image_path"]
|
|
397
|
+
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError(
|
|
400
|
+
f"[ERROR] {image_id} not found in patch or parent predictions."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
img = Image.open(image_path)
|
|
404
|
+
|
|
405
|
+
# if image_width_resolution is specified, resize the image
|
|
406
|
+
if image_width_resolution:
|
|
407
|
+
new_width = int(image_width_resolution)
|
|
408
|
+
rescale_factor = new_width / img.width
|
|
409
|
+
new_height = int(img.height * rescale_factor)
|
|
410
|
+
img = img.resize((new_width, new_height), Image.LANCZOS)
|
|
411
|
+
|
|
412
|
+
fig = plt.figure(figsize=figsize)
|
|
413
|
+
ax = plt.gca()
|
|
414
|
+
|
|
415
|
+
# check if grayscale
|
|
416
|
+
if len(img.getbands()) == 1:
|
|
417
|
+
plt.imshow(img, cmap="gray", vmin=0, vmax=255, zorder=1)
|
|
418
|
+
else:
|
|
419
|
+
plt.imshow(img, zorder=1)
|
|
420
|
+
|
|
421
|
+
for instance in preds[image_id]:
|
|
422
|
+
polygon = np.array(instance[0].exterior.coords.xy)
|
|
423
|
+
center = instance[0].centroid.coords.xy
|
|
424
|
+
patch = patches.Polygon(polygon.T, edgecolor=border_color, facecolor="none")
|
|
425
|
+
ax.add_patch(patch)
|
|
426
|
+
ax.text(
|
|
427
|
+
center[0][0], center[1][0], instance[1], fontsize=8, color=text_color
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
plt.axis("off")
|
|
431
|
+
plt.title(image_id)
|
|
432
|
+
|
|
433
|
+
if return_fig:
|
|
434
|
+
return fig
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapreader
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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
|
|
@@ -34,6 +34,9 @@ mapreader/load/loader.py
|
|
|
34
34
|
mapreader/process/__init__.py
|
|
35
35
|
mapreader/process/post_process.py
|
|
36
36
|
mapreader/process/process.py
|
|
37
|
+
mapreader/spot_text/__init__.py
|
|
38
|
+
mapreader/spot_text/deepsolo_runner.py
|
|
39
|
+
mapreader/spot_text/dptext_detr_runner.py
|
|
37
40
|
mapreader/utils/__init__.py
|
|
38
41
|
mapreader/utils/compute_and_save_stats.py
|
|
39
42
|
mapreader/utils/slice_parallel.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|