mapreader 1.3.2__tar.gz → 1.3.4__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.3.2 → mapreader-1.3.4}/PKG-INFO +1 -1
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/_version.py +3 -3
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/images.py +18 -8
- mapreader-1.3.4/mapreader/spot_text/deepsolo_runner.py +335 -0
- mapreader-1.3.4/mapreader/spot_text/dptext_detr_runner.py +153 -0
- mapreader-1.3.2/mapreader/spot_text/dptext_detr_runner.py → mapreader-1.3.4/mapreader/spot_text/runner_base.py +176 -150
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/PKG-INFO +1 -1
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/SOURCES.txt +1 -0
- mapreader-1.3.2/mapreader/spot_text/deepsolo_runner.py +0 -643
- {mapreader-1.3.2 → mapreader-1.3.4}/LICENSE +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/README.md +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/__main__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/annotator.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/utils.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/classifier.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/custom_models.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/datasets.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/load_annotations.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/data_structures.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/downloader.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/downloader_utils.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/sheet_downloader.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/tile_loading.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/tile_merging.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/geo_utils.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/loader.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/post_process.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/process.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/spot_text/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/__init__.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/compute_and_save_stats.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/slice_parallel.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/dependency_links.txt +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/entry_points.txt +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/not-zip-safe +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/requires.txt +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/top_level.txt +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/setup.cfg +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/setup.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_annotator.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_geo_pipeline.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_import.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_post_processing.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_sheet_downloader.py +0 -0
- {mapreader-1.3.2 → mapreader-1.3.4}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapreader
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
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
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-
|
|
11
|
+
"date": "2024-07-05T11:21:59+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.3.
|
|
14
|
+
"full-revisionid": "add823a499b1d0763327f9d56962c2a6af0e2d42",
|
|
15
|
+
"version": "1.3.4"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -990,6 +990,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
990
990
|
output_format: str | None = "png",
|
|
991
991
|
rewrite: bool | None = False,
|
|
992
992
|
verbose: bool | None = False,
|
|
993
|
+
overlap: int = 0,
|
|
993
994
|
) -> None:
|
|
994
995
|
"""
|
|
995
996
|
Patchify all images in the specified ``tree_level`` and (if ``add_to_parents=True``) add the patches to the MapImages instance's ``images`` dictionary.
|
|
@@ -1024,6 +1025,8 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1024
1025
|
verbose : bool, optional
|
|
1025
1026
|
If True, progress updates will be printed throughout, by default
|
|
1026
1027
|
``False``.
|
|
1028
|
+
overlap : int, optional
|
|
1029
|
+
Fractional overlap between patches, by default ``0``.
|
|
1027
1030
|
|
|
1028
1031
|
Returns
|
|
1029
1032
|
-------
|
|
@@ -1088,6 +1091,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1088
1091
|
output_format=output_format,
|
|
1089
1092
|
rewrite=rewrite,
|
|
1090
1093
|
verbose=verbose,
|
|
1094
|
+
overlap=overlap,
|
|
1091
1095
|
)
|
|
1092
1096
|
|
|
1093
1097
|
def _patchify_by_pixel(
|
|
@@ -1100,6 +1104,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1100
1104
|
output_format: str | None = "png",
|
|
1101
1105
|
rewrite: bool | None = False,
|
|
1102
1106
|
verbose: bool | None = False,
|
|
1107
|
+
overlap: int | None = 0,
|
|
1103
1108
|
):
|
|
1104
1109
|
"""Patchify one image and (if ``add_to_parents=True``) add the patch to the MapImages instance's ``images`` dictionary.
|
|
1105
1110
|
|
|
@@ -1123,6 +1128,8 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1123
1128
|
verbose : bool, optional
|
|
1124
1129
|
If True, progress updates will be printed throughout, by default
|
|
1125
1130
|
``False``.
|
|
1131
|
+
overlap : int, optional
|
|
1132
|
+
Fractional overlap between patches, by default ``0``.
|
|
1126
1133
|
"""
|
|
1127
1134
|
tree_level = self._get_tree_level(image_id)
|
|
1128
1135
|
|
|
@@ -1140,15 +1147,14 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1140
1147
|
|
|
1141
1148
|
height, width = img.height, img.width
|
|
1142
1149
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1150
|
+
x = 0
|
|
1151
|
+
while x < width:
|
|
1152
|
+
y = 0
|
|
1153
|
+
while y < height:
|
|
1145
1154
|
max_x = min(x + patch_size, width)
|
|
1146
1155
|
max_y = min(y + patch_size, height)
|
|
1147
1156
|
|
|
1148
|
-
|
|
1149
|
-
min_y = y
|
|
1150
|
-
|
|
1151
|
-
patch_id = f"patch-{min_x}-{min_y}-{max_x}-{max_y}-#{image_id}#.{output_format}"
|
|
1157
|
+
patch_id = f"patch-{x}-{y}-{max_x}-{max_y}-#{image_id}#.{output_format}"
|
|
1152
1158
|
patch_path = os.path.join(path_save, patch_id)
|
|
1153
1159
|
patch_path = os.path.abspath(patch_path)
|
|
1154
1160
|
|
|
@@ -1158,7 +1164,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1158
1164
|
)
|
|
1159
1165
|
|
|
1160
1166
|
else:
|
|
1161
|
-
patch = img.crop((
|
|
1167
|
+
patch = img.crop((x, y, max_x, max_y))
|
|
1162
1168
|
if max_x == width:
|
|
1163
1169
|
patch = ImageOps.pad(
|
|
1164
1170
|
patch, (patch_size, patch.height), centering=(0, 0)
|
|
@@ -1181,11 +1187,15 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
|
|
|
1181
1187
|
image_path=patch_path,
|
|
1182
1188
|
parent_path=parent_path,
|
|
1183
1189
|
tree_level="patch",
|
|
1184
|
-
pixel_bounds=(
|
|
1190
|
+
pixel_bounds=(x, y, max_x, max_y),
|
|
1185
1191
|
)
|
|
1186
1192
|
self._add_patch_coords_id(patch_id)
|
|
1187
1193
|
self._add_patch_polygons_id(patch_id)
|
|
1188
1194
|
|
|
1195
|
+
overlap_pixels = int(patch_size * overlap)
|
|
1196
|
+
y = y + patch_size - overlap_pixels
|
|
1197
|
+
x = x + patch_size - overlap_pixels
|
|
1198
|
+
|
|
1189
1199
|
def _patchify_by_pixel_square(
|
|
1190
1200
|
self,
|
|
1191
1201
|
image_id: str,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
import pickle
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import adet
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise ImportError(
|
|
10
|
+
"[ERROR] Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from adet.config import get_cfg
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from detectron2.engine import DefaultPredictor
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise ImportError("[ERROR] Please install Detectron2")
|
|
21
|
+
|
|
22
|
+
from shapely import LineString, MultiPolygon, Polygon
|
|
23
|
+
|
|
24
|
+
# first assert we are using the deep solo version of adet
|
|
25
|
+
if adet.__version__ != "0.2.0-deepsolo":
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"[ERROR] Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .runner_base import Runner
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DeepSoloRunner(Runner):
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
patch_df: pd.DataFrame = None,
|
|
37
|
+
parent_df: pd.DataFrame = None,
|
|
38
|
+
cfg_file: str
|
|
39
|
+
| pathlib.Path = "./DeepSolo/configs/R_50/IC15/finetune_150k_tt_mlt_13_15_textocr.yaml",
|
|
40
|
+
weights_file: str
|
|
41
|
+
| pathlib.Path = "./ic15_res50_finetune_synth-tt-mlt-13-15-textocr.pth",
|
|
42
|
+
device: str = "cpu",
|
|
43
|
+
) -> None:
|
|
44
|
+
# setup the dataframes
|
|
45
|
+
self.patch_df = patch_df
|
|
46
|
+
self.parent_df = parent_df
|
|
47
|
+
|
|
48
|
+
# set up predictions as dictionaries
|
|
49
|
+
self.patch_predictions = {}
|
|
50
|
+
self.parent_predictions = {}
|
|
51
|
+
self.geo_predictions = {}
|
|
52
|
+
|
|
53
|
+
# setup the config
|
|
54
|
+
cfg = get_cfg() # get a fresh new config
|
|
55
|
+
cfg.merge_from_file(cfg_file)
|
|
56
|
+
cfg.MODEL.WEIGHTS = weights_file
|
|
57
|
+
cfg.MODEL.DEVICE = device
|
|
58
|
+
|
|
59
|
+
self.voc_size = cfg.MODEL.TRANSFORMER.VOC_SIZE
|
|
60
|
+
self.use_customer_dictionary = cfg.MODEL.TRANSFORMER.CUSTOM_DICT
|
|
61
|
+
if self.voc_size == 96:
|
|
62
|
+
self.CTLABELS = [
|
|
63
|
+
" ",
|
|
64
|
+
"!",
|
|
65
|
+
'"',
|
|
66
|
+
"#",
|
|
67
|
+
"$",
|
|
68
|
+
"%",
|
|
69
|
+
"&",
|
|
70
|
+
"'",
|
|
71
|
+
"(",
|
|
72
|
+
")",
|
|
73
|
+
"*",
|
|
74
|
+
"+",
|
|
75
|
+
",",
|
|
76
|
+
"-",
|
|
77
|
+
".",
|
|
78
|
+
"/",
|
|
79
|
+
"0",
|
|
80
|
+
"1",
|
|
81
|
+
"2",
|
|
82
|
+
"3",
|
|
83
|
+
"4",
|
|
84
|
+
"5",
|
|
85
|
+
"6",
|
|
86
|
+
"7",
|
|
87
|
+
"8",
|
|
88
|
+
"9",
|
|
89
|
+
":",
|
|
90
|
+
";",
|
|
91
|
+
"<",
|
|
92
|
+
"=",
|
|
93
|
+
">",
|
|
94
|
+
"?",
|
|
95
|
+
"@",
|
|
96
|
+
"A",
|
|
97
|
+
"B",
|
|
98
|
+
"C",
|
|
99
|
+
"D",
|
|
100
|
+
"E",
|
|
101
|
+
"F",
|
|
102
|
+
"G",
|
|
103
|
+
"H",
|
|
104
|
+
"I",
|
|
105
|
+
"J",
|
|
106
|
+
"K",
|
|
107
|
+
"L",
|
|
108
|
+
"M",
|
|
109
|
+
"N",
|
|
110
|
+
"O",
|
|
111
|
+
"P",
|
|
112
|
+
"Q",
|
|
113
|
+
"R",
|
|
114
|
+
"S",
|
|
115
|
+
"T",
|
|
116
|
+
"U",
|
|
117
|
+
"V",
|
|
118
|
+
"W",
|
|
119
|
+
"X",
|
|
120
|
+
"Y",
|
|
121
|
+
"Z",
|
|
122
|
+
"[",
|
|
123
|
+
"\\",
|
|
124
|
+
"]",
|
|
125
|
+
"^",
|
|
126
|
+
"_",
|
|
127
|
+
"`",
|
|
128
|
+
"a",
|
|
129
|
+
"b",
|
|
130
|
+
"c",
|
|
131
|
+
"d",
|
|
132
|
+
"e",
|
|
133
|
+
"f",
|
|
134
|
+
"g",
|
|
135
|
+
"h",
|
|
136
|
+
"i",
|
|
137
|
+
"j",
|
|
138
|
+
"k",
|
|
139
|
+
"l",
|
|
140
|
+
"m",
|
|
141
|
+
"n",
|
|
142
|
+
"o",
|
|
143
|
+
"p",
|
|
144
|
+
"q",
|
|
145
|
+
"r",
|
|
146
|
+
"s",
|
|
147
|
+
"t",
|
|
148
|
+
"u",
|
|
149
|
+
"v",
|
|
150
|
+
"w",
|
|
151
|
+
"x",
|
|
152
|
+
"y",
|
|
153
|
+
"z",
|
|
154
|
+
"{",
|
|
155
|
+
"|",
|
|
156
|
+
"}",
|
|
157
|
+
"~",
|
|
158
|
+
]
|
|
159
|
+
elif self.voc_size == 37:
|
|
160
|
+
self.CTLABELS = [
|
|
161
|
+
"a",
|
|
162
|
+
"b",
|
|
163
|
+
"c",
|
|
164
|
+
"d",
|
|
165
|
+
"e",
|
|
166
|
+
"f",
|
|
167
|
+
"g",
|
|
168
|
+
"h",
|
|
169
|
+
"i",
|
|
170
|
+
"j",
|
|
171
|
+
"k",
|
|
172
|
+
"l",
|
|
173
|
+
"m",
|
|
174
|
+
"n",
|
|
175
|
+
"o",
|
|
176
|
+
"p",
|
|
177
|
+
"q",
|
|
178
|
+
"r",
|
|
179
|
+
"s",
|
|
180
|
+
"t",
|
|
181
|
+
"u",
|
|
182
|
+
"v",
|
|
183
|
+
"w",
|
|
184
|
+
"x",
|
|
185
|
+
"y",
|
|
186
|
+
"z",
|
|
187
|
+
"0",
|
|
188
|
+
"1",
|
|
189
|
+
"2",
|
|
190
|
+
"3",
|
|
191
|
+
"4",
|
|
192
|
+
"5",
|
|
193
|
+
"6",
|
|
194
|
+
"7",
|
|
195
|
+
"8",
|
|
196
|
+
"9",
|
|
197
|
+
]
|
|
198
|
+
else:
|
|
199
|
+
with open(self.use_customer_dictionary, "rb") as fp:
|
|
200
|
+
self.CTLABELS = pickle.load(fp)
|
|
201
|
+
# voc_size includes the unknown class, which is not in self.CTABLES
|
|
202
|
+
assert int(self.voc_size - 1) == len(
|
|
203
|
+
self.CTLABELS
|
|
204
|
+
), f"voc_size is not matched dictionary size, got {int(self.voc_size - 1)} and {len(self.CTLABELS)}."
|
|
205
|
+
|
|
206
|
+
# setup the predictor
|
|
207
|
+
self.predictor = DefaultPredictor(cfg)
|
|
208
|
+
|
|
209
|
+
def get_patch_predictions(
|
|
210
|
+
self,
|
|
211
|
+
outputs: dict,
|
|
212
|
+
return_dataframe: bool = False,
|
|
213
|
+
min_ioa: float = 0.7,
|
|
214
|
+
) -> dict | pd.DataFrame:
|
|
215
|
+
"""Post process the model outputs to get patch predictions.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
outputs : dict
|
|
220
|
+
The outputs from the model.
|
|
221
|
+
return_dataframe : bool, optional
|
|
222
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
223
|
+
min_ioa : float, optional
|
|
224
|
+
The minimum intersection over area to consider two polygons the same, by default 0.7
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
dict or pd.DataFrame
|
|
229
|
+
A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
|
|
230
|
+
"""
|
|
231
|
+
# key for predictions
|
|
232
|
+
image_id = outputs["image_id"]
|
|
233
|
+
self.patch_predictions[image_id] = []
|
|
234
|
+
|
|
235
|
+
# get instances
|
|
236
|
+
instances = outputs["instances"].to("cpu")
|
|
237
|
+
ctrl_pnts = instances.ctrl_points.numpy()
|
|
238
|
+
scores = instances.scores.tolist()
|
|
239
|
+
recs = instances.recs
|
|
240
|
+
bd_pts = np.asarray(instances.bd)
|
|
241
|
+
|
|
242
|
+
self._post_process(image_id, ctrl_pnts, scores, recs, bd_pts)
|
|
243
|
+
self._deduplicate(image_id, min_ioa=min_ioa)
|
|
244
|
+
|
|
245
|
+
if return_dataframe:
|
|
246
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
247
|
+
return self.patch_predictions
|
|
248
|
+
|
|
249
|
+
def _process_ctrl_pnt(self, pnt):
|
|
250
|
+
points = pnt.reshape(-1, 2)
|
|
251
|
+
return points
|
|
252
|
+
|
|
253
|
+
def _ctc_decode_recognition(self, rec):
|
|
254
|
+
last_char = "###"
|
|
255
|
+
s = ""
|
|
256
|
+
for c in rec:
|
|
257
|
+
c = int(c)
|
|
258
|
+
if c < self.voc_size - 1:
|
|
259
|
+
if last_char != c:
|
|
260
|
+
if self.voc_size == 37 or self.voc_size == 96:
|
|
261
|
+
s += self.CTLABELS[c]
|
|
262
|
+
last_char = c
|
|
263
|
+
else:
|
|
264
|
+
s += str(chr(self.CTLABELS[c]))
|
|
265
|
+
last_char = c
|
|
266
|
+
else:
|
|
267
|
+
last_char = "###"
|
|
268
|
+
return s
|
|
269
|
+
|
|
270
|
+
def _post_process(self, image_id, ctrl_pnts, scores, recs, bd_pnts, alpha=0.4):
|
|
271
|
+
for ctrl_pnt, score, rec, bd in zip(ctrl_pnts, scores, recs, bd_pnts):
|
|
272
|
+
# draw polygons
|
|
273
|
+
if bd is not None:
|
|
274
|
+
bd = np.hsplit(bd, 2)
|
|
275
|
+
bd = np.vstack([bd[0], bd[1][::-1]])
|
|
276
|
+
polygon = Polygon(bd).buffer(0)
|
|
277
|
+
|
|
278
|
+
if isinstance(polygon, MultiPolygon):
|
|
279
|
+
polygon = polygon.convex_hull
|
|
280
|
+
|
|
281
|
+
# draw center lines
|
|
282
|
+
line = self._process_ctrl_pnt(ctrl_pnt)
|
|
283
|
+
line = LineString(line)
|
|
284
|
+
|
|
285
|
+
# draw text
|
|
286
|
+
text = self._ctc_decode_recognition(rec)
|
|
287
|
+
if self.voc_size == 37:
|
|
288
|
+
text = text.upper()
|
|
289
|
+
# text = "{:.2f}: {}".format(score, text)
|
|
290
|
+
text = f"{text}"
|
|
291
|
+
score = f"{score:.2f}"
|
|
292
|
+
|
|
293
|
+
self.patch_predictions[image_id].append([polygon, text, score])
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _dict_to_dataframe(
|
|
297
|
+
preds: dict,
|
|
298
|
+
geo: bool = False,
|
|
299
|
+
parent: bool = False,
|
|
300
|
+
) -> pd.DataFrame:
|
|
301
|
+
"""Convert the predictions dictionary to a pandas DataFrame.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
preds : dict
|
|
306
|
+
A dictionary of predictions.
|
|
307
|
+
geo : bool, optional
|
|
308
|
+
Whether the dictionary is georeferenced coords (or pixel bounds), by default True
|
|
309
|
+
parent : bool, optional
|
|
310
|
+
Whether the dictionary is at parent level, by default False
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
pd.DataFrame
|
|
315
|
+
A pandas DataFrame containing the predictions.
|
|
316
|
+
"""
|
|
317
|
+
if geo:
|
|
318
|
+
columns = ["polygon", "crs", "text", "score"]
|
|
319
|
+
else:
|
|
320
|
+
columns = ["polygon", "text", "score"]
|
|
321
|
+
|
|
322
|
+
if parent:
|
|
323
|
+
columns.append("patch_id")
|
|
324
|
+
|
|
325
|
+
preds_df = pd.concat(
|
|
326
|
+
pd.DataFrame(
|
|
327
|
+
preds[k],
|
|
328
|
+
index=np.full(len(preds[k]), k),
|
|
329
|
+
columns=columns,
|
|
330
|
+
)
|
|
331
|
+
for k in preds.keys()
|
|
332
|
+
)
|
|
333
|
+
preds_df.index.name = "image_id"
|
|
334
|
+
preds_df.reset_index(inplace=True) # reset index to get image_id as a column
|
|
335
|
+
return preds_df
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import adet
|
|
7
|
+
except ImportError:
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"[ERROR] Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from adet.config import get_cfg
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from detectron2.engine import DefaultPredictor
|
|
18
|
+
except ImportError:
|
|
19
|
+
raise ImportError("[ERROR] Please install Detectron2")
|
|
20
|
+
|
|
21
|
+
from shapely import MultiPolygon, Polygon
|
|
22
|
+
|
|
23
|
+
# first assert we are using the dptext detr version of adet
|
|
24
|
+
if adet.__version__ != "0.2.0-dptext-detr":
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"[ERROR] Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .runner_base import Runner
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DPTextDETRRunner(Runner):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
patch_df: pd.DataFrame = None,
|
|
36
|
+
parent_df: pd.DataFrame = None,
|
|
37
|
+
cfg_file: str
|
|
38
|
+
| pathlib.Path = "./DPText-DETR/configs/DPText_DETR/ArT/R_50_poly.yaml",
|
|
39
|
+
weights_file: str | pathlib.Path = "./art_final.pth",
|
|
40
|
+
device: str = "cpu",
|
|
41
|
+
) -> None:
|
|
42
|
+
# setup the dataframes
|
|
43
|
+
self.patch_df = patch_df
|
|
44
|
+
self.parent_df = parent_df
|
|
45
|
+
|
|
46
|
+
# set up predictions as dictionaries
|
|
47
|
+
self.patch_predictions = {}
|
|
48
|
+
self.parent_predictions = {}
|
|
49
|
+
self.geo_predictions = {}
|
|
50
|
+
|
|
51
|
+
# setup the config
|
|
52
|
+
cfg = get_cfg() # get a fresh new config
|
|
53
|
+
cfg.merge_from_file(cfg_file)
|
|
54
|
+
cfg.MODEL.WEIGHTS = weights_file
|
|
55
|
+
cfg.MODEL.DEVICE = device
|
|
56
|
+
|
|
57
|
+
# setup the predictor
|
|
58
|
+
self.predictor = DefaultPredictor(cfg)
|
|
59
|
+
|
|
60
|
+
def get_patch_predictions(
|
|
61
|
+
self,
|
|
62
|
+
outputs: dict,
|
|
63
|
+
return_dataframe: bool = False,
|
|
64
|
+
min_ioa: float = 0.7,
|
|
65
|
+
) -> dict | pd.DataFrame:
|
|
66
|
+
"""Post process the model outputs to get patch predictions.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
outputs : dict
|
|
71
|
+
The outputs from the model.
|
|
72
|
+
return_dataframe : bool, optional
|
|
73
|
+
Whether to return the predictions as a pandas DataFrame, by default False
|
|
74
|
+
min_ioa : float, optional
|
|
75
|
+
The minimum intersection over area to consider two polygons the same, by default 0.7
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
dict or pd.DataFrame
|
|
80
|
+
A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
|
|
81
|
+
"""
|
|
82
|
+
# key for predictions
|
|
83
|
+
image_id = outputs["image_id"]
|
|
84
|
+
self.patch_predictions[image_id] = []
|
|
85
|
+
|
|
86
|
+
# get instances
|
|
87
|
+
instances = outputs["instances"].to("cpu")
|
|
88
|
+
scores = instances.scores.tolist()
|
|
89
|
+
pred_classes = instances.pred_classes.tolist()
|
|
90
|
+
bd_pts = np.asarray(instances.polygons)
|
|
91
|
+
|
|
92
|
+
self._post_process(image_id, scores, pred_classes, bd_pts)
|
|
93
|
+
self._deduplicate(image_id, min_ioa=min_ioa)
|
|
94
|
+
|
|
95
|
+
if return_dataframe:
|
|
96
|
+
return self._dict_to_dataframe(self.patch_predictions, geo=False)
|
|
97
|
+
return self.patch_predictions
|
|
98
|
+
|
|
99
|
+
def _post_process(self, image_id, scores, pred_classes, bd_pnts):
|
|
100
|
+
for score, _pred_class, bd in zip(scores, pred_classes, bd_pnts):
|
|
101
|
+
# draw polygons
|
|
102
|
+
if bd is not None:
|
|
103
|
+
bd = bd.reshape(-1, 2)
|
|
104
|
+
polygon = Polygon(bd).buffer(0)
|
|
105
|
+
|
|
106
|
+
if isinstance(polygon, MultiPolygon):
|
|
107
|
+
polygon = polygon.convex_hull
|
|
108
|
+
|
|
109
|
+
score = f"{score:.2f}"
|
|
110
|
+
|
|
111
|
+
self.patch_predictions[image_id].append([polygon, score])
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _dict_to_dataframe(
|
|
115
|
+
preds: dict,
|
|
116
|
+
geo: bool = False,
|
|
117
|
+
parent: bool = False,
|
|
118
|
+
) -> pd.DataFrame:
|
|
119
|
+
"""Convert the predictions dictionary to a pandas DataFrame.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
preds : dict
|
|
124
|
+
A dictionary of predictions.
|
|
125
|
+
geo : bool, optional
|
|
126
|
+
Whether the dictionary is georeferenced coords (or pixel bounds), by default True
|
|
127
|
+
parent : bool, optional
|
|
128
|
+
Whether the dictionary is at the parent level, by default False
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
pd.DataFrame
|
|
133
|
+
A pandas DataFrame containing the predictions.
|
|
134
|
+
"""
|
|
135
|
+
if geo:
|
|
136
|
+
columns = ["polygon", "crs", "score"]
|
|
137
|
+
else:
|
|
138
|
+
columns = ["polygon", "score"]
|
|
139
|
+
|
|
140
|
+
if parent:
|
|
141
|
+
columns.append("patch_id")
|
|
142
|
+
|
|
143
|
+
preds_df = pd.concat(
|
|
144
|
+
pd.DataFrame(
|
|
145
|
+
preds[k],
|
|
146
|
+
index=np.full(len(preds[k]), k),
|
|
147
|
+
columns=columns,
|
|
148
|
+
)
|
|
149
|
+
for k in preds.keys()
|
|
150
|
+
)
|
|
151
|
+
preds_df.index.name = "image_id"
|
|
152
|
+
preds_df.reset_index(inplace=True)
|
|
153
|
+
return preds_df
|